RSMS API results — implementation plan (Parquet / DuckDB migration era)
Archived. Checklist referred to migrating reads away from DuckDB toward Azure Table Storage + blobs — that migration is finished in production code.
app/results/plus OpenAPI/docsare the behavioral reference. rsms-master-tasklist captures open follow-ups.
Historical context: duckdb and RSMS-results Parquet reads were retired in favor of Azure Table Storage + blob JSON. The checklist sections below recorded the migration era; app/results/, ssot/rsms-ssot.json, ssot/rsms-domain-units-ssot.md, and route mapping sibling rsms-api-results-duckdb-design (also archived).
1. Scope (v1)
| In scope | Out of scope (v1) |
|---|---|
GET .../cxplt/by-hour ( PlumeByTime row) | Legacy chart 2 / 5 full CTPLT parity — product backlog |
GET .../cxplt/by-mile ( PlumeByMile row) | Ad-hoc client queries; claiming legacy chart 2 or 5 semantics from CXPLT-style slices alone |
GET .../mass-balance ( mass_balance.json blob) | Caching layer beyond minimal (optional later) |
GET .../edge-profile, GET .../edge-timeseries (JSON) | plume_river_mile_axis returns data (rsms-master-tasklist) |
2. Dependencies and configuration
2.1 Python (rsms-api-fastapi)
azure-data-tablesandazure-storage-blob(JSON blobs).duckdbis not used for RSMS scenario results.- Reuse connection string / managed identity patterns from
app/azure_storage.pywhere applicable.
2.2 Settings (app/config.py / env)
| Variable | Purpose |
|---|---|
AZURE_STORAGE_CONNECTION_STRING | Blob + Table (same account); used by results for JSON blobs and plume Table rows |
RSMS_RESULTS_BLOB_CONTAINER | Container for edge_timeseries.json, mass_balance.json (default rsms-output; align with writer pipeline and SSOT configKeys) |
Document in .env.example with comments; no secrets committed.
3. Module layout (implemented)
rsms-api-fastapi/app/
results/
__init__.py
router.py # prefix /riverbasins/{id}/scenarios/{id}/results
constants.py # Table names (PlumeByTime / PlumeByMile)
keys.py # Partition/Row key helpers
decode.py # float32 buffers → series
plume_index.py # hour/mile → table indices
models.py # Pydantic responses + meta
service.py # Table get_entity + blob download + orchestration
main.py: app.include_router(results_router, ...) behind RESULTS_API_ENABLED.
4. Implementation phases
Phase A — Skeleton + parity with SSOT
- Load
ssot/rsms-ssot.jsonat startup (or embed version string constant synced manually for v1). - Add
results/router.pywith three routes returning stub JSON (emptyseries) to validate paths and OpenAPI. - Add Pydantic models matching
apiResponsessection of SSOT.
Exit: GET routes visible in /docs under correct prefix.
Phase B — Table + blob resolution
table_source.py/blob_source.py: Givenriverbasin_id,scenario_id, build Table partition/row keys (SSOT) and blob paths formass_balance.json/edge_timeseries.json.- Read path: Single-row
get_entityforPlumeByTime/PlumeByMile; download JSON blobs to memory or temp file as needed. 404if entity/blob missing (orGET /scenarios/{id}fails first).
Exit: Unit tests with Azurite, in-memory fakes, or checked-in tiny float32 buffers (no production secrets in CI).
Phase C — Decode + JSON assembly
plume_index.py/service.py: Maphour→time_index,miles→mile_indexwith SSOT rounding;get_entityon PlumeByTime / PlumeByMile with bound keys.- Binary payloads:
numpy.frombuffer(..., dtype=float32); validate length M or T from metadata. - Mass balance: parse
mass_balance.json; computetotal_massin Python aspmass + dzmassper SSOTderived.
Exit: Integration test: fixture buffers/JSON → JSON series / rows length > 0.
Phase D — Mass balance + scenario merge
GET /scenarios/{scenario_id}(existing) — fetch scenario forspill_quantity/quantityfield when available (pounds per domain SSOT).- Embed
spill_quantity_lbinmass-balanceresponse (nullif absent).
Exit: Response matches apiResponses.massBalance in SSOT.
Phase E — Async / performance
- Run Table/blob I/O (sync clients) in
asyncio.to_thread()(orrun_in_executor) so event loop is not blocked. - Optional: request-level timeout (e.g. 30s).
Exit: Load test note only; no premature optimization.
Phase F — Edge endpoint (optional)
GET .../edge-profile→ 501 + JSON{"reason": "CTPLT not available", "deferred": true}per API design §4.4.
Exit: Frontend can branch without throwing.
Phase G — Documentation and SSOT sync
- Link
docs/archive/rsms-api-results-implementation-plan.mdfromrsms-api-fastapi/README.mdif present. - On each behavioral change, update
ssot/rsms-ssot.jsonand bumpmeta.schemaVersion.
5. Security and validation
- UUID validation for path parameters (FastAPI
UUIDtype). - Authorization: reuse
get_current_user/ scenario access rules if/WHEN scenarios are tenant-scoped (mirrorGET /scenarios/{id}behavior). - No user-supplied storage keys; validated UUIDs and fixed endpoint maps only.
6. Testing strategy
| Layer | Approach |
|---|---|
| Keys / decode | Partition keys, index mapping, float32 buffer lengths. |
| Service | Table row fixtures or JSON blobs under tests/fixtures/results/. |
| E2E | Optional: Azurite or test storage account. |
7. Rollback
- Feature flag
RESULTS_API_ENABLED(env) to disable router registration if needed.
8. References
| Doc | Role |
|---|---|
| rsms-api-results-duckdb-design | Archived route ↔ chart mapping (hist.) |
| rsms-results-table-storage-rework-plan | Active deploy / ops phased plan |
ssot/rsms-ssot.json | Columns, API shapes |
ssot/rsms-domain-units-ssot.md | Units and domain terms |
Implementation plan — RSMS API results reads.